/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.spring.web;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.Ordered;

import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import java.io.PrintWriter;
import java.io.StringWriter;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * Path matching exception resolver.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @since 20-may-2009 11:18:54
 */
public class PathMatchingExceptionResolver implements HandlerExceptionResolver, Ordered {
	/**
	 * Logger for this class.
	 */
	private static final Log LOG = LogFactory.getLog(PathMatchingExceptionResolver.class);

	/**
	 * The url to role mapping.
	 */
	private Map<String, String> mappings;

	/**
	 * The path matcher.
	 */
	private PathMatcher pathMatcher;

	/**
	 * The order.
	 */
	private int order;

/**
   * Constructor.
   */
	public PathMatchingExceptionResolver() {
		this.pathMatcher = new AntPathMatcher();
	}

	/**
	 * Sets the order.
	 *
	 * @param order the order.
	 */
	public void setOrder(int order) {
		this.order = order;
	}

	/**
	 * Sets the path mappings.
	 *
	 * @param mappings the path mappings.
	 */
	public void setMappings(Map<String, String> mappings) {
		this.mappings = mappings;
	}

	/**
	 * Sets the path matcher.
	 *
	 * @param pathMatcher the path matcher.
	 */
	public void setPathMatcher(final PathMatcher pathMatcher) {
		this.pathMatcher = pathMatcher;
	}

	/**
	 * Try to resolve the given exception that got thrown during on handler execution, returning a ModelAndView
	 * that represents a specific error page if appropriate.
	 *
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler the executed handler, or <code>null</code> if none chosen at the time of the exception (for
	 * 				example, if multipart resolution failed)
	 * @param ex the exception that got thrown during handler execution
	 *
	 * @return a corresponding ModelAndView to forward to, or <code>null</code> for default processing
	 */
	public ModelAndView resolveException(
		HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
		// Log exception, both at debug log level and at warn level, if desired.
		if(LOG.isDebugEnabled()) {
			LOG.debug("Resolving exception from handler [" + handler + "]: " + ex);
		}

		StringBuilder builder = new StringBuilder();

		LOG.error(builder.toString(), ex);

		// Expose ModelAndView for chosen error view.
		String viewName = determineViewName(ex, request);

		if(viewName != null) {
			return getModelAndView(viewName, ex, request);
		} else {
			return null;
		}
	}

	/**
	 * Determine the view name.
	 *
	 * @param ex the exception.
	 * @param request the request.
	 *
	 * @return the view name.
	 */
	protected String determineViewName(final Exception ex, final HttpServletRequest request) {
		String path = request.getRequestURI();
		String contextPath = request.getContextPath();

		final String subPath = path.substring(contextPath.length());

		for(Map.Entry<String, String> p : mappings.entrySet()) {
			if(pathMatcher.match(p.getKey(), subPath)) {
				return p.getValue();
			}
		}

		return null;
	}

	/**
	 * Retrieves the model and view.
	 *
	 * @param viewName the view name.
	 * @param ex the exception.
	 * @param request the request.
	 *
	 * @return the model and view.
	 */
	protected ModelAndView getModelAndView(final String viewName, final Exception ex, final HttpServletRequest request) {
		ModelAndView mv = new ModelAndView(viewName);

		mv.addObject("exception", ex);

		StringWriter writer = new StringWriter();
		PrintWriter printer = new PrintWriter(writer);

		ex.printStackTrace(printer);
		printer.flush();

		mv.addObject("stacktrace", writer.toString());

		return mv;
	}

	/**
	 * Return the order value of this object, with a higher value meaning greater in terms of sorting.<p>Normally
	 * starting with 0 or 1, with {@link #LOWEST_PRECEDENCE} indicating greatest. Same order values will result in
	 * arbitrary positions for the affected objects.</p>
	 *  <p>Higher value can be interpreted as lower priority, consequently the first object has highest priority
	 * (somewhat analogous to Servlet "load-on-startup" values).</p>
	 *  <p>Note that order values below 0 are reserved for framework purposes. Application-specified values should
	 * always be 0 or greater, with only framework components (internal or third-party) supposed to use lower values.</p>
	 *
	 * @return the order value
	 *
	 * @see #LOWEST_PRECEDENCE
	 */
	public int getOrder() {
		return order;
	}
}
